﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;

namespace Heimdallr.Async
{

    /// <summary>
    /// Provides a set of methods for inaccurate schedule.  
    /// </summary> 
    public static class SchedulePool
    {
        #region Field

        private readonly static Lazy<Timer> timer = new Lazy<Timer>(() => new Timer(ScheduleCallback), true);
        private readonly static ConcurrentDictionary<Guid, Schedule> schedules = new ConcurrentDictionary<Guid, Schedule>();

        #endregion

        #region Property

        internal static Timer Timer
        {
            get { return timer.Value; }
        }

        internal static IEnumerable<Schedule> Callbacks
        {
            get { return schedules.Values; }
        }

        #endregion

        #region Method

        /// <summary>
        /// Registers a callback method with speficied interval. 
        /// </summary>
        /// <param name="callback">A callback method executed when the interval elapses.</param>
        /// <param name="seconds">The interval at which to execute <paramref name="callback"/>.</param>
        /// <returns>A GUID which can be used on <see cref="Unregister"/>, <see cref="GetDuration"/> or <see cref="ChangeDuration"/> methods.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="callback"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="seconds"/> must greater than 0.</exception>
        public static Guid Register(TimerCallback callback, int seconds)
        {
            return Register(callback, seconds, null);
        }

        /// <summary>
        /// Registers a callback method with speficied interval. 
        /// </summary>
        /// <param name="callback">A callback method executed when the interval elapses.</param>
        /// <param name="seconds">The interval at which to execute <paramref name="callback"/>.</param>
        /// <param name="state">The optional user-supplied state object.</param>
        /// <returns>A GUID which can be used on <see cref="Unregister"/>, <see cref="GetDuration"/> or <see cref="ChangeDuration"/> methods.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="callback"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="seconds"/> must greater than 0.</exception>
        public static Guid Register(TimerCallback callback, int seconds, object state)
        {
            if (callback == null)
                throw new ArgumentNullException("callback");

            if (seconds < 1)
                throw new ArgumentOutOfRangeException("seconds", "Must greater than 0.");

            var item = new Schedule(callback, seconds, state);

            if (schedules.TryAdd(item.ID, item) && !schedules.IsEmpty)
                Timer.Change(1000, 1000);

            return item.ID;
        }

        /// <summary>
        /// Unregisters a callback method.
        /// </summary>
        /// <param name="id">A GUID witch is generated by <see cref="Register"/> method.</param> 
        public static void Unregister(Guid id)
        {
            Schedule schedule;

            if (schedules.TryRemove(id, out schedule) && schedules.IsEmpty)
                Timer.Change(Timeout.Infinite, Timeout.Infinite);
        }

        /// <summary>
        /// Gets the duration of a callback, using speficied GUID.
        /// </summary>
        /// <param name="id">A GUID witch is generated by <see cref="Register"/> method.</param> 
        /// <returns>If <paramref name="id"/> exists, returns the duration. Otherwise the return value is 0.</returns>
        public static int GetDuration(Guid id)
        {
            Schedule schedule;

            if (schedules.TryGetValue(id, out schedule))
                return schedule.Duration;
            else
                return 0;
        }

        /// <summary>
        /// Changes the duration of a callback, using specified GUID.
        /// </summary>
        /// <param name="id">A GUID witch is generated by <see cref="Register"/> method.</param> 
        /// <param name="seconds">The interval at which to execute <paramref name="callback"/>.</param>
        /// <returns>If <paramref name="id"/> exists, returns true. Otherwise the return value is false.</returns>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="seconds"/> must greater than 0.</exception>
        public static bool ChangeDuration(Guid id, int seconds)
        {
            if (seconds < 1)
                throw new ArgumentOutOfRangeException("seconds", "Must greater than 0.");

            Schedule schedule;

            if (!schedules.TryGetValue(id, out schedule))
                return false;

            schedule.Duration = seconds;

            return true;
        }

        #endregion

        #region Callback

        private static void ScheduleCallback(object state)
        {
            foreach (var schedule in schedules.Values)
                schedule.TryInvoke();
        }

        #endregion

        #region Other

        internal class Schedule
        {
            #region 欄位

            private static readonly Lazy<Random> rand = new Lazy<Random>(() => new Random(DateTime.Now.Millisecond), true);

            private readonly TimerCallback callback;
            private readonly object state;
            private readonly Guid id;

            private int duration;
            private int current;

            #endregion

            #region 屬性

            protected static Random Rand
            {
                get { return rand.Value; }
            }

            public TimerCallback Callback
            {
                get { return this.callback; }
            }

            public object State
            {
                get { return this.state; }
            }

            public Guid ID
            {
                get { return this.id; }
            }

            public int Duration
            {
                get { return this.duration; }
                set { this.duration = value; }
            }

            #endregion

            #region 建構子

            public Schedule(TimerCallback callback, int duration)
                : this(callback, duration, null)
            {

            }

            public Schedule(TimerCallback callback, int duration, object state)
            {
                this.callback = callback;
                this.duration = duration;
                this.state = state;

                this.current = Rand.Next(1, duration + 1);
                this.id = Guid.NewGuid();
            }

            #endregion

            #region 方法

            public bool TryInvoke()
            {
                if (this.current < this.duration)
                {
                    this.current++;
                    return false;
                }
                else
                {
                    this.current = 1;
                    ThreadPool.QueueUserWorkItem(new WaitCallback(this.callback), this.state);
                    return true;
                }
            }

            #endregion
        } 

        #endregion
    }
}
